package org.fhm.zdte.client.api;

import org.fhm.substrate.annotation.BeanEnable;
import org.fhm.substrate.annotation.BeanInitial;
import org.fhm.substrate.annotation.Component;
import org.fhm.substrate.annotation.Setup;
import org.fhm.substrate.service.LoggerHandler;
import org.fhm.substrate.standard.ILogger;
import org.fhm.zdte.client.ability.*;
import org.fhm.zdte.client.cmd.OnlineClientCmd;
import org.fhm.zdte.client.config.ClientInitialConfiguration;
import org.fhm.zdte.client.exception.ClientException;
import org.fhm.zdte.common.ability.IGetIdentification;
import org.fhm.zdte.common.ability.IGetResponseResult;
import org.fhm.zdte.common.ability.IGetServerAddress;
import org.fhm.zdte.common.ability.IRequestTimeout;
import org.fhm.zdte.common.constant.protocol.ConnectStatus;
import org.fhm.zdte.common.constant.protocol.ContentType;
import org.fhm.zdte.common.constant.protocol.PacketType;
import org.fhm.zdte.common.constant.subcmd.ConnectCmd;
import org.fhm.zdte.common.exception.ProtocolException;
import org.fhm.zdte.common.pojo.Address;
import org.fhm.zdte.common.pojo.Request;
import org.fhm.zdte.common.pojo.Result;
import org.fhm.zdte.common.pool.ThreadPoolFactory;
import org.fhm.zdte.common.service.api.AbstractApi;
import org.fhm.zdte.common.service.api.CommonThreadManager;
import org.fhm.zdte.common.standard.IChannelDispatcher;
import org.fhm.zdte.common.standard.IJsonConvertor;
import org.fhm.zdte.common.standard.ISendMessageManager;
import org.fhm.zdte.common.standard.IThreadManager;
import org.fhm.zdte.common.util.CommonUtil;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * API provided by the client side.
 *
 * @author 谭波
 * @since 2023/11/9
 */
@Component
@SuppressWarnings("unused")
public class ClientApi extends AbstractApi implements IGetServerAddress {

    private final ILogger logger = LoggerHandler.getLogger(ClientApi.class);

    private Address serverAddress;

    @Setup
    private IThreadManager iThreadManager;

    @Setup
    private ClientInitialConfiguration clientInitialConfiguration;

    @Setup
    private ThreadPoolFactory threadPoolFactory;

    @Setup
    private IIsOnline isOnline;

    @Setup
    private IGetConnectResult iGetConnectResult;

    @Setup
    private IChannelDispatcher iChannelDispatcher;

    @Setup
    private IJsonConvertor iJsonConvertor;

    @Setup
    private CommonUtil commonUtil;

    @Setup
    private IConnectTimeoutRun iConnectTimeoutRun;

    @Setup
    private IConnectInitialize iConnectInitialize;

    @Setup
    private Map<String, ISendMessageManager> iSendMessageManagerMap;

    @Setup
    private IGetResponseResult iGetResponseResult;

    @Setup
    private IGetIdentification iGetIdentification;

    @Setup
    private IGetConnectState iGetConnectState;

    @Setup
    private Map<String, IRequestTimeout> iRequestTimeoutMap;

    @Setup
    private IAddConnectTask iAddConnectTask;

    @Setup
    private IStartTwoStepsConnect iStartTwoStepsConnect;

    /**
     * the client goes online
     *
     * @throws ClientException client exception
     */
    public void clientOnline() throws ClientException {
        if (isOnline.isOnline())
            return;
        logger.info("the client is in the process of being launched");
        ScheduledFuture<?> task = null;
        try {
            task = threadPoolFactory
                    .submitPeriodicTask(
                            () ->
                                    sendDealTask.getTaskAndSubmit(
                                            abstractMessageWrappers
                                                    .getMessageByPacketType(
                                                            PacketType.ONLINE, null,
                                                            "", "", "",
                                                            null, null, null,
                                                            null),
                                            serverAddress
                                    ),
                            clientInitialConfiguration.getOnlineSchedule(),
                            TimeUnit.SECONDS
                    );
            boolean isWait;
            try {
                isWait = iThreadManager.waitForTime(
                        clientInitialConfiguration.getClientOnlineTimeout(),
                        TimeUnit.SECONDS,
                        CommonThreadManager.ONLINE_CLIENT_CONDITION
                );
            } catch (ProtocolException e) {
                throw new ClientException(e.getMessage());
            }
            if (!isWait) {
                iChannelDispatcher.closeAllChannelManager();
                throw new ClientException("your online request has timed out");
            }
        } finally {
            if (Objects.nonNull(task))
                commonUtil.cancelTask(task);
        }
    }

    /**
     * use the configuration key to connect
     *
     * @param target userId
     * @return result
     * @throws ClientException e
     */
    public Result<Void> connect(String target) throws ClientException {
        return connect(target, clientInitialConfiguration.getSecretKey(), true);
    }

    /**
     * Start the two-step connection.
     *
     * @param target The identification of target.
     */
    public void startTwoStepsConnection(String target) {
        try {
            connect(target, clientInitialConfiguration.getSecretKey(), false);
        } catch (ClientException e) {
            logger.error(e.getMessage(), e);
        }
    }

    /**
     * use customize key to connect
     *
     * @param target     other user numbers
     * @param secretKey  The secret key of target client side.
     * @param isTwoSteps Whether it is a second-stage connection.
     * @return connect result
     * @throws ClientException client exception
     */
    public Result<Void> connect(String target, String secretKey, boolean isTwoSteps) throws ClientException {
        if (Objects.isNull(target) || target.isEmpty())
            throw new ClientException("the target of connect can not be empty or null");
        String identification = iGetIdentification.getIdentification();
        if (target.equals(identification))
            throw new ClientException("the target identification is incorrect");
        ConnectStatus connectState = iGetConnectState.getConnectState(target);
        if (!ConnectStatus.isNotConnected(connectState))
            throw new ClientException("the target " + target + " has been connected " + connectState.getDesc());
        if (isOnline.isOnline()) {
            iConnectInitialize.connectInitialize(target);
            String messageNumber = commonUtil.getMessageNumber();
            ScheduledFuture<?> connectTask = threadPoolFactory
                    .submitPeriodicTask(
                            () ->
                                    sendDealTask.getTaskAndSubmit(
                                            abstractMessageWrappers
                                                    .getMessageByPacketType(
                                                            PacketType.CONNECT,
                                                            ConnectCmd.START_CONNECT,
                                                            identification,
                                                            target,
                                                            messageNumber,
                                                            null, null, null,
                                                            secretKey
                                                    ),
                                            serverAddress
                                    ),
                            1,
                            TimeUnit.SECONDS
                    );
            iAddConnectTask.addConnectTask(target, connectTask);
            boolean isWait;
            try {
                isWait = iThreadManager.waitForTime(
                        10,
                        TimeUnit.SECONDS,
                        messageNumber
                );
            } catch (ProtocolException e) {
                throw new ClientException(e.getMessage());
            }
            Result<Void> startConnectResult;
            if (isWait)
                startConnectResult = iGetConnectResult.getConnectResult(target);
            else {
                iConnectTimeoutRun.connectTimeoutRun(target);
                throw new ClientException("the connection target has gone offline");
            }
            if (Objects.nonNull(startConnectResult) && Result.isFailed(startConnectResult))
                return startConnectResult;
            logger.info("the current connection is started to {}", target);
            if (isWaitConnect(CommonThreadManager.CONNECT_CONDITION + messageNumber, 10)) {
                logger.info("successfully connect to {}", target);
                return iGetConnectResult.getConnectResult(target);
            } else {
                iConnectTimeoutRun.connectTimeoutRun(target);
                if (isTwoSteps) {
                    logger.info("start the two-step connect to {}", target);
                    iStartTwoStepsConnect.startTwoStepsConnect(target, messageNumber);
                    if (isWaitConnect(CommonThreadManager.CONNECT_CONDITION + messageNumber, 25)) {
                        Result<Void> connectResult = iGetConnectResult.getConnectResult(target);
                        if (Objects.nonNull(connectResult)) {
                            if (Result.isSuccessful(connectResult))
                                logger.info("successfully connect to {}", target);
                            return connectResult;
                        } else
                            throw new ClientException("connect to " + target + " time out with secret key is " + secretKey);
                    } else
                        throw new ClientException("connect to " + target + " time out with secret key is " + secretKey);
                } else
                    throw new ClientException("connect to " + target + " time out with secret key is " + secretKey);
            }
        }
        throw new ClientException("the client is not online");
    }

    private boolean isWaitConnect(String messageNumber, int time) throws ClientException {
        boolean isWait;
        try {
            isWait = iThreadManager.waitForTime(
                    time,
                    TimeUnit.SECONDS,
                    messageNumber
            );
        } catch (ProtocolException e) {
            throw new ClientException(e.getMessage());
        }
        return isWait;
    }


    /**
     * initiate a request to the server
     *
     * @param interfaceUrl  interfaceUrl
     * @param clazz         response clazz
     * @param parameters    params
     * @param messageNumber messageNumber
     * @param <T>           T
     * @return response body
     * @throws ClientException client exception
     */
    public <T> Result<T> requestServer(String interfaceUrl, Class<T> clazz, Map<String, Object> parameters, String messageNumber) throws ClientException {
        return executeRequest(OnlineClientCmd.SERVER_IDENTIFICATION, interfaceUrl, clazz, parameters, messageNumber, ContentType.TEXT);
    }

    /**
     * initiate a request to the server
     *
     * @param interfaceUrl interfaceUrl
     * @param clazz        response clazz
     * @param parameters   params
     * @param <T>          T
     * @return response body
     * @throws ClientException client exception
     */
    public <T> Result<T> requestServer(String interfaceUrl, Class<T> clazz, Map<String, Object> parameters) throws ClientException {
        return executeRequest(OnlineClientCmd.SERVER_IDENTIFICATION, interfaceUrl, clazz, parameters, commonUtil.getMessageNumber(), ContentType.TEXT);
    }

    /**
     * initiate a request to the server
     *
     * @param target        target user number
     * @param interfaceUrl  interfaceUrl
     * @param clazz         response clazz
     * @param parameters    params
     * @param messageNumber messageNumber
     * @param type          {@link ContentType}
     * @param <T>           T
     * @return response body
     * @throws ClientException client exception
     */
    public <T> Result<T> request(String target, String interfaceUrl, Class<T> clazz, Map<String, Object> parameters, String messageNumber, ContentType type) throws ClientException {
        ConnectStatus connectState = iGetConnectState.getConnectState(target);
        if (ConnectStatus.isNotConnected(connectState))
            return Result.getFailResult(null, "not connected to the target " + target);
        return executeRequest(target, interfaceUrl, clazz, parameters, messageNumber, type);
    }

    /**
     * initiate a request to the server
     *
     * @param target        target user number
     * @param interfaceUrl  interfaceUrl
     * @param clazz         response clazz
     * @param parameters    params
     * @param messageNumber messageNumber
     * @param <T>           T
     * @return response body
     * @throws ClientException client exception
     */
    public <T> Result<T> request(String target, String interfaceUrl, Class<T> clazz, Map<String, Object> parameters, String messageNumber) throws ClientException {
        ConnectStatus connectState = iGetConnectState.getConnectState(target);
        if (ConnectStatus.isNotConnected(connectState))
            return Result.getFailResult(null, "not connected to the target " + target);
        return executeRequest(target, interfaceUrl, clazz, parameters, messageNumber, ContentType.TEXT);
    }

    /**
     * initiate a request to the server
     *
     * @param target       target user number
     * @param interfaceUrl interfaceUrl
     * @param clazz        response clazz
     * @param parameters   params
     * @param <T>          T
     * @return response body
     * @throws ClientException client exception
     */
    public <T> Result<T> request(String target, String interfaceUrl, Class<T> clazz, Map<String, Object> parameters) throws ClientException {
        ConnectStatus connectState = iGetConnectState.getConnectState(target);
        if (ConnectStatus.isNotConnected(connectState))
            return Result.getFailResult(null, "not connected to the target " + target);
        return executeRequest(target, interfaceUrl, clazz, parameters, commonUtil.getMessageNumber(), ContentType.TEXT);
    }

    @SuppressWarnings("unchecked")
    private <T> Result<T> executeRequest(
            String target, String interfaceUrl,
            Class<T> clazz, Map<String, Object> parameters,
            String messageNumber, @SuppressWarnings("SameParameterValue") ContentType contentType
    ) throws ClientException {
        if (!isOnline.isOnline())
            throw new ClientException("the client side is offline");
        if (Objects.isNull(target) || target.isEmpty())
            throw new ClientException("the target of connect can not be empty or null");
        String identification = iGetIdentification.getIdentification();
        if (target.equals(identification))
            throw new ClientException("the target identification is incorrect");
        Request request = new Request();
        if (Objects.nonNull(parameters))
            request.setParams(parameters);
        else
            request.setParams(new HashMap<>());
        request.setInterfaceUrl(interfaceUrl);
        request.setContentType(contentType);
        threadPoolFactory.submitTask(
                ThreadPoolFactory.TRANSFORM_THREAD_POOL,
                () -> {
                    ISendMessageManager iSendMessageManager = iSendMessageManagerMap.get(contentType.getName());
                    iSendMessageManager.initial(request, messageNumber, target);
                }
        );
        boolean isWait;
        try {
            isWait = iThreadManager.waitForTime(clientInitialConfiguration.getRequestTimeout(), TimeUnit.SECONDS, messageNumber);
        } catch (ProtocolException e) {
            throw new ClientException(e.getMessage());
        }
        if (isWait) {
            Result<String> responseResult = iGetResponseResult.getResponseResult(messageNumber);
            if (Objects.isNull(clazz) || String.class.isAssignableFrom(clazz))
                return (Result<T>) responseResult;
            return Result.reSetResult(
                    responseResult, content -> iJsonConvertor.fromJson((String) content, clazz)
            );
        } else {
            iRequestTimeoutMap.values().forEach(run -> run.connectTimeoutRun(target, messageNumber));
            throw new ClientException("request to servlet that interface is " + interfaceUrl + " time out");
        }
    }

    @Override
    public Address getServerAddress() {
        return serverAddress;
    }

    @BeanInitial
    private void beanInitial() {
        String ip;
        int serverPort;
        serverAddress = new Address(
                ip = clientInitialConfiguration.getServerIp(),
                serverPort = clientInitialConfiguration.getServerPort()
        );
        logger.info("client online target {}:{}", ip, serverPort);
    }

    @BeanEnable
    private void beanEnable() {
        int heartbeatSchedule = clientInitialConfiguration.getHeartbeatSchedule();
        logger.info("the heartbeat detection cycle time is {} seconds", heartbeatSchedule);
        logger.info("the heartbeat detection timeout period is {} seconds",
                clientInitialConfiguration.getHeartbeatTimeout());
        threadPoolFactory.submitPeriodicTask(
                () ->
                        sendDealTask.getTaskAndSubmit(
                                abstractMessageWrappers.getMessageByPacketType(
                                        PacketType.HEARTBEAT,
                                        null, "",
                                        "", "",
                                        null, null, null,
                                        null),
                                null
                        ),
                heartbeatSchedule,
                TimeUnit.SECONDS
        );
    }

}
